﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace LaTeXGrapher
{
    enum TokenType
    {
        None,
        Comment,
        WhiteSpace,
        EndOfLine,
        Operator,
        Identifier
    }

    class Token
    {
        public string Data { get; private set; }
        public TokenType TokenType { get; private set; }

        public Token(string data, TokenType type)
        {
            Data = data;
            TokenType = type;
        }

        public bool IsEndOfLine
        {
            get { return TokenType == TokenType.EndOfLine; }
        }

        public bool IsWhiteSpace
        {
            get { return TokenType == TokenType.WhiteSpace; }
        }
    }

    class Tokenizer
    {
        enum State
        {
            None,
            InComment,
            InIdentifier,
            InWhiteSpace
        }

        State _state = State.None;
        StringBuilder _sb = new StringBuilder();
        bool _bEOF = false;
        IEnumerable<char> _source;
        IEnumerator<char> _data;
        Stack<char> _stack = new Stack<char>();

        public Tokenizer(IEnumerable<char> source)
        {
            _source = source;
            _data = _source.GetEnumerator();
        }

        public char GetChar()
        {
            if (_stack.Count > 0)
                return _stack.Pop();
            if (!_data.MoveNext())
            {
                if (_bEOF)
                    return '\0';
                _bEOF = true;
                return '\n';
            }
            return _data.Current;
        }

        public void PushChar(char c)
        {
            _stack.Push(c);
        }

        public string GetUntil(char final)
        {
            StringBuilder sb = new StringBuilder();
            for (; ; )
            {
                char c = GetChar();
                if (c == final)
                    return sb.ToString();
                if (c == '\0')
                    throw new Exception(string.Format("Unexpected end of input while looking for {0}.", PrettyChar(final)));
                sb.Append(c);
            }
        }

        static string PrettyChar(char c)
        {
            if (c == '\n')
                return "end of line";
            if (char.IsWhiteSpace(c))
                return "white space";
            if (char.IsControl(c))
                return string.Format("0x{0:XXXX}", c);
            return "'" + c + "'";
        }

        public static bool IsOperator(char c)
        {
            return "()[]{}^\"\\|:!@#$^&*-+=<>?/~`,;".Contains(c);
        }

        public static bool IsIdentifier(char c)
        {
            return char.IsLetterOrDigit(c) || "_'".Contains(c);
        }

        public static bool IsIdentifier(string str)
        {
            return ((IEnumerable<char>)str).All(IsIdentifier);
        }

        public Token GetToken()
        {
            for (; ; )
            {
                char c = GetChar();
                switch (_state)
                {
                    default:
                    case State.None:
                        switch (c)
                        {
                            case '\0':
                                return null;
                            case '%':
                                _state = State.InComment;
                                break;
                            case '\n':
                                return CharToken('\n', State.None, TokenType.EndOfLine);
                            case '\r':
                                continue; // ignore
                            default:
                                if (char.IsWhiteSpace(c))
                                {
                                    _state = State.InWhiteSpace;
                                }
                                else if (IsOperator(c))
                                    return CharToken(c, State.None, TokenType.Operator);
                                else
                                {
                                    _state = State.InIdentifier;
                                }
                                break;
                        }
                        break;
                    case State.InComment:
                        if (c == '\n')
                            return BuildToken(State.None, TokenType.Comment);
                        break;
                    case State.InIdentifier:
                        if (!IsIdentifier(c))
                        {
                            PushChar(c);
                            return BuildToken(State.None, TokenType.Identifier);
                        }
                        break;
                    case State.InWhiteSpace:
                        if (c == '\n' || !char.IsWhiteSpace(c))
                        {
                            PushChar(c);
                            return BuildToken(State.None, TokenType.WhiteSpace);
                        }
                        break;
                }
                _sb.Append(c);
            }
            return null;
        }

        private Token BuildToken(State returnState, TokenType type)
        {
            _state = returnState;
            string str = _sb.ToString();
            _sb = new StringBuilder();
            return new Token(str, type);
        }

        private Token CharToken(char c, State returnState, TokenType type)
        {
            _state = returnState;
            Debug.Assert(_sb.Length == 0);
            _sb = new StringBuilder();
            return new Token(c.ToString(), type);
        }
    }
}
